import datetime
import logging
import os
import pathlib
import queue
import re
import time
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor
from hashlib import md5
from queue import Queue
from shutil import copy
from typing import Union, Callable, TypeVar, List, Tuple

import yaml
from watchdog.events import FileSystemEventHandler, FileCreatedEvent, FileModifiedEvent
from watchdog.observers.polling import PollingObserver

# 1.5 seconds
ALLOW_DOWNLOADING_NOT_UPDATE_INTERVAL = 1.5 * 60 * 1000
QUEUE_WAIT_FOR_DATA_TIMEOUT = 1
EVENT_TYPE = TypeVar('EVENT_TYPE', FileCreatedEvent, FileModifiedEvent)
EVENT_QUEUE: "Queue[Filter]" = Queue()
TASK_QUEUE: "Queue[Task]" = Queue()
logger = logging.getLogger("main")


def read_yaml(filename: pathlib.Path) -> dict:
    with open(str(filename), encoding='utf-8') as f:
        try:
            yaml_content = yaml.safe_load(f)
            return yaml_content if isinstance(yaml_content, dict) else {}
        except yaml.YAMLError as e:
            logger.error(e)
            return {}


class Task:
    id: str
    task: Callable
    is_valid: bool = True
    data: tuple = ()

    def __init__(self, task: Callable, data: tuple):
        self.task = task
        self.data = data
        self.id = self.generate_task_id()

    def set_invalid_task(self):
        self.is_valid = False

    def get_task_state(self) -> bool:
        return self.is_valid

    def generate_task_id(self) -> str:
        s = time.time() * 1000
        hash_obj = md5(str(s).encode("utf-8"))
        return hash_obj.hexdigest()

    def doit(self):
        logger.info(f"[+] task[id={self.id}] was running")
        if self.task and isinstance(self.task, Callable):
            res = self.task(*self.data)
            logger.info(f"[+] task[id={self.id}] done.")
            return res
        logger.info(f"[!] invalid task[id={self.id}]")
        return -1


class FileWatch:
    def __init__(self, path: Union[pathlib.Path, str], event_handler):
        self._path = path
        self._event_handler = event_handler
        self.observer: PollingObserver
        self.end: bool = False

    def start(self):
        event_handler = self._event_handler()
        self.observer = PollingObserver()
        self.observer.schedule(
            event_handler,
            self._path,
            recursive=True
        )
        self.observer.start()
        logger.info(f"启动文件监控任务[{self.observer.name}-{self.observer.native_id}].")
        logger.info(f"监控路径为: {self._path}")

    def stop(self):
        logger.info("停止文件监控任务")
        if self.observer:
            self.observer.stop()
            self.observer.join()
            logger.info(f"监控任务[{self.observer.name}-{self.observer.native_id}]停止. ")

    def is_alive(self) -> bool:
        if self.observer: self.observer.is_alive()


class Filter:
    def filter(self):
        raise NotImplementedError("Must implement it")

    def get_url_str(self):
        raise NotImplementedError("Must implement it")


class RomFileFilter(Filter):
    filename_pool = defaultdict(float)

    def __init__(self, event: EVENT_TYPE):
        self._path: Union[str, pathlib.Path] = ''
        self.vehicle_projects: list = []
        self._event = event

    def filter(self) -> Union[str, pathlib.Path]:
        logger.info(f"筛选有效文件中...")
        self._process_event()
        path = self._path if self.allow_downloading() else ''
        logger.info(f"找到完成更新文件, 文件为: {path}")
        return path

    def allow_downloading(self) -> bool:
        time_n = self.__class__.filename_pool.get(self._path, 0)
        ret = time.time() * 1000 - time_n > ALLOW_DOWNLOADING_NOT_UPDATE_INTERVAL
        logger.info(f"文件更新完毕，达到监控时间间隔: {ret}")
        return ret

    def _process_event(self):
        self._path = self._event.src_path
        if self._path not in self.__class__.filename_pool: self.__class__.filename_pool[self._path] = time.time() * 1000
        logger.info(f"监控到文件[{self._path}].")
        return self._path

    def update_vehicle_project(self, vehicle_projects: list):
        self.vehicle_projects = vehicle_projects
        logger.info(f"更新监控车型为: {self.vehicle_projects}")

    def get_url_str(self):
        return self._path


class RomFileEventHandler(FileSystemEventHandler):
    def on_created(self, event):
        if not event.is_directory:
            EVENT_QUEUE.put(RomFileFilter(event))
        logger.debug(f"file created{event}")

    def on_modified(self, event):
        if not event.is_directory:
            EVENT_QUEUE.put(RomFileFilter(event))
        logger.debug(f"file modified: {event}")


class DownloadFile:
    def __init__(self, url: str, config_filename):
        self.url = url
        self.config_filename = str(config_filename)
        self.http_client = None

    def download(self):
        dest_dir = self._make_file_path()
        try:
            copy(self.url, dest_dir)
        except Exception as e:
            logger.error(f"[!] 从路径{self.url}到{dest_dir}拷贝文件失败[{e}]！")
            return 1

    def _make_file_path(self):
        if not self.config_filename:
            logger.warning(f"用于存储rom的本地目录不存在,请检查!")
            return
        filename = self.url.split('\\')[-1]
        return pathlib.Path(self.config_filename).joinpath(filename)

    def update_url(self, url):
        self.url = url


def _to_date() -> str:
    return time.strftime("%Y%m%d")


def _get_date() -> datetime.date:
    return datetime.date.today()


def _generte_watch_urls(root_url: str, v_projects: list, countrys: list) -> Tuple[list, list]:
    urls, not_exists_urls = [], []
    daily = r"daily_binary\{vehicle_type}\{country}\{date}"
    binary = r"binary\{vehicle_type}\{country}\Release"
    for v in v_projects:
        for c in countrys:
            u_d = pathlib.Path(root_url).joinpath(daily.format(vehicle_type=v, country=c, date=_to_date()))
            if not u_d.exists():
                not_exists_urls.append(u_d)
                continue
            urls.append(str(u_d))
            logger.debug(f"查找的路径为: {u_d}")
    return urls, not_exists_urls


def _start_watch(root_url: str, v_projects: list, countrys: list) -> Tuple[List[FileWatch], list]:
    watchs, urls, not_exist_urls = [], [], []

    try:
        while len(urls) == 0:
            urls, not_exist_urls = _generte_watch_urls(root_url, v_projects, countrys)
            time.sleep(1)

        for u in urls:
            logger.debug(f"找到url路径为: {u}")
            watch = FileWatch(u, RomFileEventHandler)
            watch.start()
            watchs.append(watch)
    except PermissionError as e:
        logger.error(f"无目录访问权限, {e}.")
    logger.info(f"urls任务都启动完毕, urls: {len(urls)}")
    return watchs, not_exist_urls


def produce(path: Union[str, pathlib.Path], url: str, v_projects: list):
    if isinstance(path, str): path = pathlib.Path(path)
    if not path.exists():
        os.makedirs(path.absolute(), exist_ok=True)
        logger.info(f"本地路径不存在{path},重新创建.")
    prevision = _get_date()
    watchs, not_exist_urls = _start_watch(url, v_projects, ['ZH', 'EU'])
    if len(watchs) == 0: return
    logger.info("启动任务监听程序.")
    try:
        while True:
            if (prevision - _get_date()).days <= -1:
                logger.info("新一天，更新日期重新启动文件监控程序")
                prevision = _get_date()
                [w.stop() for w in watchs if w.is_alive()]
                _watchs, not_exist_urls = _start_watch(url, v_projects, ['ZH', 'EU'])
                watchs.extend(_watchs)
            try:
                len_not_url = len(not_exist_urls)
                for _ in range(len_not_url):
                    n_u = not_exist_urls.pop(0)
                    if n_u.exists():
                        logger.info(f"找到新路径: {n_u}")
                        watch = FileWatch(n_u, RomFileEventHandler)
                        watch.start()
                        watchs.append(watch)
                        continue
                    not_exist_urls.append(n_u)

                rom_filter = EVENT_QUEUE.get(timeout=QUEUE_WAIT_FOR_DATA_TIMEOUT)
                file_url = rom_filter.filter()
                if not file_url:
                    EVENT_QUEUE.put(rom_filter)
                    logger.info(f"url还未允许下载，稍后再试. {rom_filter.get_url_str()}")
                    continue
                if not is_valid_url(str(file_url), v_projects, ['ZH', 'EU']):
                    logger.info(f"文件还未就绪/不是一个需要文件，稍后再下载")
                    continue
                downloader = DownloadFile(file_url, path)
                task = Task(downloader.download, ())
                TASK_QUEUE.put(task)
            except queue.Empty:
                pass
            except Exception as e:
                logger.error(f"匹配路径中发生错误{e}")
    except KeyboardInterrupt:
        pass
    logger.info("调度任务监听结束.")


def schedule():
    logger.info("启动调度程序")
    pool = ThreadPoolExecutor(os.cpu_count())
    future_tasks = []
    try:
        while True:
            for f_l in future_tasks:
                future, task = list(f_l.items())[0]
                if future.done():
                    r = future.result()
                    if r == 1:
                        TASK_QUEUE.put(task)
                        logger.info(f"重新执行下载失败的任务taskid: {task.id}")
            task = TASK_QUEUE.get()
            r = pool.submit(task.doit)
            future_tasks.append({r: task})
    except KeyboardInterrupt:
        pass
    pool.shutdown()
    logger.info("调度程序结束")


def is_valid_url(path: str, v_projects: list, country: list) -> bool:
    date = _to_date()
    pattern = r"^.+[daily_binary|binary].?[" + \
              "|".join(v_projects) + \
              r"].+[" + \
              "|".join(country) + \
              "].+" + \
              date + \
              r".+USERDEBUG.+_QFIL\.zip$"
    part_path = re.compile(pattern)
    part_path = part_path.search(path)
    logger.debug(f"匹配监控到的rom路径re规则式为: {pattern}")
    logger.info(f"匹配到有效rom包路径[{part_path is not None}]")
    logger.info(f"rom包路径为: [{path}]")
    return part_path is not None


if __name__ == '__main__':
    d = DownloadFile(
        r"",
        r""
    )
    # d.download()
    is_valid_url(
        r"",
        [''])
